From 9eb8e80f4f736c4edffa650c685d1f170ca51aa1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 15 May 2025 01:19:49 +0000 Subject: (대표님) 구매 요청사항 반영한 통합 rfq / 필터 개인화 / po-rfq MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/evcp/(evcp)/po-rfq/page.tsx | 86 +++++++++++ .../partners/(partners)/rfq-all/[id]/page.tsx | 80 ++++++++++ app/[lng]/partners/(partners)/rfq-all/page.tsx | 171 +++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 app/[lng]/evcp/(evcp)/po-rfq/page.tsx create mode 100644 app/[lng]/partners/(partners)/rfq-all/[id]/page.tsx create mode 100644 app/[lng]/partners/(partners)/rfq-all/page.tsx (limited to 'app/[lng]') diff --git a/app/[lng]/evcp/(evcp)/po-rfq/page.tsx b/app/[lng]/evcp/(evcp)/po-rfq/page.tsx new file mode 100644 index 00000000..dfaa7708 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/po-rfq/page.tsx @@ -0,0 +1,86 @@ +import { Suspense } from "react" +import { getPORfqs } from "@/lib/procurement-rfqs/services" +import { searchParamsCache } from "@/lib/procurement-rfqs/validations" +import { Shell } from "@/components/shell" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import RFQContainer from "@/components/po-rfq/po-rfq-container" + +interface RfqPageProps { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; + title?: string; + description?: string; +} + +export default async function RfqPage({ + searchParams, + title = "발주용 견적", + description = "SAP으로부터 전송된 발주용 견적을 관리할 수 있습니다.", +}: RfqPageProps) { + // searchParams를 await하여 resolve + const resolvedSearchParams = await searchParams; + + // 서버 액션: RFQ 데이터 가져오기 + async function fetchRfqData(params: any) { + "use server" + + try { + // URL 파라미터를 추출하고 필요한 형식으로 변환 + const parsedParams = searchParamsCache.parse(params); + + // RFQ 데이터 가져오기 + const data = await getPORfqs(parsedParams) + + return data + } catch (error) { + console.error("RFQ 데이터 조회 오류:", error) + // 에러 발생 시 빈 결과 반환 + return { data: [], pageCount: 0, total: 0 } + } + } + + // 현재 resolvedSearchParams를 파싱하여 초기 데이터 로드 + const initialParams = { + page: resolvedSearchParams.page?.toString() || "1", + perPage: resolvedSearchParams.perPage?.toString() || "10", + sort: resolvedSearchParams.sort?.toString() || JSON.stringify([{ id: "updatedAt", desc: true }]), + filters: resolvedSearchParams.filters?.toString() || null, + joinOperator: resolvedSearchParams.joinOperator?.toString() || "and", + basicFilters: resolvedSearchParams.basicFilters?.toString() || null, + basicJoinOperator: resolvedSearchParams.basicJoinOperator?.toString() || "and", + search: resolvedSearchParams.search?.toString() || "", + } + + // 초기 데이터 로드 + const initialData = await fetchRfqData(initialParams) + + return ( + +
+
+
+

+ {title} +

+
+
+
+ + + } + > + + +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-all/[id]/page.tsx b/app/[lng]/partners/(partners)/rfq-all/[id]/page.tsx new file mode 100644 index 00000000..c8858704 --- /dev/null +++ b/app/[lng]/partners/(partners)/rfq-all/[id]/page.tsx @@ -0,0 +1,80 @@ +// app/vendor/quotations/[id]/page.tsx - 견적 응답 페이지 +import { Metadata } from "next" +import { notFound } from "next/navigation" +import db from "@/db/db"; +import { eq } from "drizzle-orm" +import { procurementVendorQuotations } from "@/db/schema" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import VendorQuotationEditor from "@/lib/procurement-rfqs/vendor-response/quotation-editor"; + + +interface PageProps { + params: { + id: string + } +} + +export async function generateMetadata({ params }: PageProps): Promise { + return { + title: "견적서 응답", + description: "RFQ에 대한 견적서 작성 및 제출", + } +} + +export default async function VendorQuotationPage({ params }: PageProps) { + const quotationId = parseInt(params.id) + + if (isNaN(quotationId)) { + notFound() + } + + // 인증 확인 + const session = await getServerSession(authOptions); + + if (!session?.user) { + return ( +
+
+

로그인이 필요합니다

+

견적서 응답을 위해 로그인해주세요.

+
+
+ ) + } + + // 견적서 정보 가져오기 + const quotation = await db.query.procurementVendorQuotations.findFirst({ + where: eq(procurementVendorQuotations.id, quotationId), + with: { + rfq: true, // 관계 설정 필요 + vendor: true, // 관계 설정 필요 + items: true, // 관계 설정 필요 + } + }) + + if (!quotation) { + notFound() + } + + // 벤더 권한 확인 (필요한 경우) + const isAuthorized = session.user.domain === "partners" && + session.user.companyId === quotation.vendorId + + if (!isAuthorized) { + return ( +
+
+

접근 권한이 없습니다

+

이 견적서에 대한 권한이 없습니다.

+
+
+ ) + } + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/app/[lng]/partners/(partners)/rfq-all/page.tsx b/app/[lng]/partners/(partners)/rfq-all/page.tsx new file mode 100644 index 00000000..e7dccb02 --- /dev/null +++ b/app/[lng]/partners/(partners)/rfq-all/page.tsx @@ -0,0 +1,171 @@ +// app/vendor/quotations/page.tsx +import * as React from "react"; +import Link from "next/link"; +import { Metadata } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { LogIn } from "lucide-react"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { Shell } from "@/components/shell"; +import { getValidFilters } from "@/lib/data-table"; +import { type SearchParams } from "@/types/table"; +import { searchParamsVendorRfqCache } from "@/lib/procurement-rfqs/validations"; +import { getQuotationStatusCounts, getVendorQuotations } from "@/lib/procurement-rfqs/services"; +import { VendorQuotationsTable } from "@/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table"; + +export const metadata: Metadata = { + title: "견적 목록", + description: "진행 중인 견적서 목록", +}; + +interface IndexPageProps { + searchParams: Promise +} + + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsVendorRfqCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + // 인증 확인 + const session = await getServerSession(authOptions); + + // 로그인 확인 + if (!session || !session.user) { + return ( + +
+
+

+ 견적 목록 +

+

+ 진행 중인 견적서 목록을 확인하고 관리합니다. +

+
+
+ +
+
+

로그인이 필요합니다

+

+ 견적서를 확인하려면 먼저 로그인하세요. +

+ +
+
+
+ ); + } + + // 벤더 ID 확인 + const vendorId = session.user.companyId ? String(session.user.companyId) : "0"; + + // 벤더 권한 확인 + if (session.user.domain !== "partners") { + return ( + +
+
+

+ 접근 권한 없음 +

+
+
+
+
+

벤더 계정이 필요합니다

+

+ 벤더 계정으로 로그인해주세요. +

+
+
+
+ ); + } + + // 데이터 가져오기 + const quotationsPromise = getVendorQuotations({ + ...search, + filters: validFilters + }, vendorId); + + // 상태별 개수 가져오기 + const statusCountsPromise = getQuotationStatusCounts(vendorId); + + // 모든 프로미스 병렬 실행 + const promises = Promise.all([quotationsPromise]); + const statusCounts = await statusCountsPromise; + + return ( + +
+
+

견적 목록

+

+ 진행 중인 견적서 목록을 확인하고 관리합니다. +

+
+
+ +
+ + + 전체 견적 + + +
+ {Object.values(statusCounts).reduce((sum, count) => sum + count, 0)}건 +
+
+
+ + + 작성 중 + + +
{statusCounts.Draft || 0}건
+
+
+ + + 제출됨 + + +
+ {(statusCounts.Submitted || 0) + (statusCounts.Revised || 0)}건 +
+
+
+ + + 승인됨 + + +
{statusCounts.Accepted || 0}건
+
+
+
+ + + } + > + + +
+ ); +} \ No newline at end of file -- cgit v1.2.3